program EscapeFromSwinburne;
uses SwinGameAPI, SysUtils;

const
	//The game's name
	GAMENAME = 'Escape From Swinburne';

	//Resolution size
	RESX = 800;
	RESY = 600;
	
	//Viewport Move At X Line
	VWPRTMOVEATX = 320;

	//Movement Speeds
	GRAVITY   = -1; 	//(Reversed Y Vector) Y axis
	LEFTWALK  = -3;		//X axis
	RIGHTWALK = 3; 		//X axis
	LEFTRUN   = -2;		//X axis
	RIGHTRUN  = 2;		//X axis
	JUMP      = 1.2;	//(Reversed Y Vector) Y axis
	JUMPCOUNTER = 1;
	
	//Jetpack settings
	MAXJETPACKFUEL     = 500;
	JETPACKRECHARGE    = 4;
	JETPACKUSEPERCYCLE = 10;
	
	//Game speed and load balancing rates
	//(In number of microseconds since the last update)
	DATAUPDATERATE = 16000; //Data update every 16ms (62.5fps)
	RENDERMAXRATE  = 8000;  //Max render update every 8ms (31.25fps)
	RENDERMINRATE  = 32000; //Min render update every 32ms (125fps)
	
	//World Object types
	BACKGROUNDTYPE = 0;
	PLAYERTYPE     = 1;
	STATICTYPE     = 2;
	LEFTRIGHTAI    = 3;
	COINAI         = 4;
	JETFUELCONTAI  = 5;
	
	//Animation Modes
	STANDLEFT     = 0;
	STANDRIGHT    = 1;
	WALKLEFT      = 2;
	WALKRIGHT     = 3;
	JUMPHMOVLEFT  = 4;
	JUMPHMOVRIGHT = 5;
	JUMPLEFT      = 6;
	JUMPRIGHT     = 7;
	
	//Sounds
	BACKGROUNDMUSIC  = 0;
	WALKSOUND        = 1;
	JETPACKSOUND     = 2;
	COINSOUND        = 3;
	JETFUELCONTSOUND = 4;
	GAMEWINSOUND     = 5;
	
	//Game over types
	NOTOVER    = 0;
	PLAYERLOSE = 1;
	PLAYERWIN  = 2;
	
	//Coin's worth
	COINWORTH = 10;
	
	//Max Jet Fuel Containers
	MAXJETFUELCONTS = 8;
	
	//UI Indexes
	JETFUELCONTICON = 0;
	UIBACKGROUNDIMG = 1;
	
type
	AnimationIndexes = record
		startIndex, endIndex : Integer;	
	end;

	AnimationProperties = record
		currentAniMode : Integer;
		indexes : Array [STANDLEFT .. JUMPRIGHT] of AnimationIndexes;
	end;
	
	WorldObject = record
		spr : Sprite;
		collidable : Boolean;
		dead : Boolean;
		affectedByGravity : Boolean;
		killsPlayer : Boolean;
		jumpCounter : Integer;
		vertMvVector : Vector;
		horzMvVector : Vector;
		decayProps : DecayProperties;
		objectType : Integer;
		aniProps : AnimationProperties;
	end;
	
	FPSCalcInfo = record
		valuesArray : Array [0..9] of Single;
		arrayIndex, loopCount : Integer;
		high, low, average : Single;
	end;
	
	BackgroundPanel = record
		spr : Sprite;
		bitmap : Bitmap;
	end;

var
	backgroundWidth : Integer;
	backgroundPanels : Array of BackgroundPanel;
	objects : Array of WorldObject;
	bitmaps : Array of Bitmap;
	uiSprites : Array [JETFUELCONTICON .. UIBACKGROUNDIMG] of Sprite;
	uiBitmaps : Array [JETFUELCONTICON .. UIBACKGROUNDIMG] of Bitmap;
	sBuffers : Array [BACKGROUNDMUSIC .. GAMEWINSOUND] of SoundBuffer;
	sSources : Array [BACKGROUNDMUSIC .. GAMEWINSOUND] of SoundSource;
	
	PLAYER : Integer;
	
	jetpackFuel : Integer;
	jetFuelContainers : Integer;
	playerScore : Integer;
	
	//Viewport coordinates
	vwPrtX : Integer;
	vwPrtY : Integer;
	
	quitRequested : Boolean;
	
	//Map Options
	mapName : String;
	nextMapName : String;
	mapExitX, mapExitY, mapExitWidth, mapExitHeight : Integer;
	jetFuelContainersOnStart, playerScoreOnStart : Integer;
	
	//Controls whether the game is over or not
	gameOver : Integer;
	
	//This is used by ProcessKeyboardEvents() to make sure the 
	//F key is released before another press is registered
	fKeyPressed : Boolean; 
	
	debugDump : String;
	

	
function LoadSingleBitmap(var inputFile : Text; mapName : String): Bitmap;
var
	temp, filename : String;
	width, height, red, green, blue : Integer;
	transparentColour : Colour;
	imageHasAlpha : Boolean;
	alphaAmount : Integer;
begin
	ReadLn(inputFile, temp); //Read and throw away comment line
	
	imageHasAlpha := false;
		
	ReadLn(inputFile, filename);
	filename := mapName + '\\' + filename;
	
	ReadLn(inputFile, width);
	ReadLn(inputFile, height);
	
	ReadLn(inputFile, temp);
	if temp = 'NotTransparent' then
		transparentColour := NotTransparent
	else if temp = 'Alpha' then
	begin
		imageHasAlpha := true;
		transparentColour := NotTransparent;
		ReadLn(inputFile, alphaAmount)
	end
	else
	begin
		TryStrToInt(temp, red);
		ReadLn(inputFile, green);
		ReadLn(inputFile, blue);
		transparentColour := GetColour(red, green, blue);
	end;
	
	result := LoadBitmap(filename, width, height, imageHasAlpha, alphaAmount, transparentColour);
end;
	
procedure LoadBitmaps(mapName : string);
var
	inputFile : Text;
	index, numBitmaps : Integer;
begin
	WriteLn('Loading Bitmaps...');	

	Assign(inputFile, mapName + '\\bitmaps.txt');
	Reset(inputFile);
	
	ReadLn(inputFile, numBitmaps);
	
	SetLength(bitmaps, numBitmaps);
	
	WriteLn(numBitmaps, ' bitmaps specified');
	
	for index := 0 to (numBitmaps - 1) do
	begin
		WriteLn('Loading bitmap number ', index + 1);
		
		bitmaps[index] := LoadSingleBitmap(inputFile, mapName);
	end;
	
	Close(inputFile);
end;


function TranslateStringBool(str : string): Boolean;
begin
	if (str = 'True') or (str = 'TRUE') then result := true
	else if (str = 'False') or (str = 'FALSE') then result := false
	else raise Exception.Create('Invalid String Boolean Representation');
	
end;


function LoadWOCopies(var inputFile : Text; index : Integer): Integer;
var
	index2, total2, tempInt : Integer;
	temp : String;
begin
	ReadLn(inputFile, total2);
	
	for index2 := 1 to (total2 - 1) do
	begin
		WriteLn('Copy WO Index ', index + index2 + 1);
		objects[index + index2] := objects[index];
	end;
	
	for index2 := 0 to (total2 - 1) do
	begin
		ReadLn(inputFile, temp); //Discard
		
		ReadLn(inputFile, tempInt);
		objects[index + index2].spr.xPos := tempInt;
		ReadLn(inputFile, tempInt);
		objects[index + index2].spr.yPos := tempInt;
	end;
	
	result := total2;
end;


procedure LoadWOProperties(var inputFile : Text; index : Integer);
var
	temp : String;
	tempSingle : Single;
begin
	ReadLn(inputFile, temp);
	objects[index].collidable := TranslateStringBool(temp);
	
	ReadLn(inputFile, temp);
	objects[index].dead := TranslateStringBool(temp);
	
	ReadLn(inputFile, temp);
	objects[index].affectedByGravity := TranslateStringBool(temp);
	
	ReadLn(inputFile, temp);
	objects[index].killsPlayer := TranslateStringBool(temp);
	
	ReadLn(inputFile, tempSingle);
	objects[index].decayProps.posXDecay := tempSingle;
	ReadLn(inputFile, tempSingle);
	objects[index].decayProps.negXDecay := tempSingle;
	ReadLn(inputFile, tempSingle);
	objects[index].decayProps.posYDecay := tempSingle;
	ReadLn(inputFile, tempSingle);
	objects[index].decayProps.negYDecay := tempSingle;
end;


procedure LoadWOBitmaps(var inputFile : Text; index : Integer);
var
	index2, total2, bitmapIndex : Integer;
	
begin
	ReadLn(inputFile, total2);
	ReadLn(inputFile, bitmapIndex);
	objects[index].spr := CreateSprite(bitmaps[bitmapIndex]);
	for index2 := 2 to total2 do
	begin
		ReadLn(inputFile, bitmapIndex);
		AddBitmapToSprite(objects[index].spr, bitmaps[bitmapIndex]);
	end;
end;


procedure LoadWOType(var inputFile : Text; index : Integer);
var
	temp : String;
begin
	ReadLn(inputFile, temp);
	
	//NOTE: Put the most common types first for efficiency
	if temp = 'Static' then
		objects[index].objectType := STATICTYPE
	else if temp = 'CoinAI' then
	begin
		objects[index].objectType := COINAI
	end
	else if temp = 'JetFuelContainerAI' then
	begin
		objects[index].objectType := JETFUELCONTAI
	end
	else if temp = 'LeftRightAI' then
	begin
		objects[index].objectType := LEFTRIGHTAI
	end
	else if temp = 'Background' then
		objects[index].objectType := BACKGROUNDTYPE
	else if temp = 'Player' then
	begin
		PLAYER := index;
		objects[index].objectType := PLAYERTYPE;
	end
	else
	begin
		WriteLn('  Invalid World Object Type ' + temp); 
		objects[index].objectType := STATICTYPE;
	end;
end;


procedure LoadOneAniProperty(var inputFile : Text; out startFrameIndex, endFrameIndex : Integer);
var
	temp : String;
begin
	ReadLn(inputFile, temp); //Discard comment line
	ReadLn(inputFile, startFrameIndex);
	ReadLn(inputFile, endFrameIndex);
end;


procedure LoadWOAnimationProperties(var inputFile : Text; index : Integer);
var
	count : Integer;
begin
	for count := Low(objects[index].aniProps.indexes) to High(objects[index].aniProps.indexes) do
	begin
		LoadOneAniProperty(inputFile, objects[index].aniProps.indexes[count].startIndex, objects[index].aniProps.indexes[count].endIndex);
	end;

	objects[index].aniProps.currentAniMode := WALKRIGHT;
	objects[index].spr.currentFrame := objects[index].aniProps.indexes[WALKRIGHT].startIndex;
end;


procedure LoadWorldObjects(mapName : String);
var
	inputFile : Text;
	index, total, total2, skipLoopCounter: Integer;
	temp : String;
begin
	WriteLn('Loading World Objects...');
	
	skipLoopCounter := 0;
	
	Assign(inputFile, mapName + '\\WorldObjects.txt');
	Reset(inputFile);
	
	ReadLn(inputFile, total);
	
	WriteLn('You specified ', total, ' World Objects');
	
	SetLength(objects, 0);
	SetLength(objects, total);
	
	for index := 0 to (total - 1) do
	begin		
		if skipLoopCounter <> 0 then
		begin
			skipLoopCounter := skipLoopCounter - 1;
			continue;
		end;
		
		WriteLn('Loading WO Number ', index + 1);
	
		ReadLn(inputFile, temp); //Discard this comment line
		
		LoadWOType(inputFile, index);
				
		LoadWOBitmaps(inputFile, index);
		
		LoadWOAnimationProperties(inputFile, index);
		
		LoadWOProperties(inputFile, index);
		
		total2 := LoadWOCopies(inputFile, index);
		
		//Account for the copies in the total count
		skipLoopCounter := total2 - 1;
		
	end;
	
	Close(inputFile);
end;


procedure LoadSounds(mapName : String);
var
	inputFile : Text;
	temp : String;
	index : Integer;
begin
	WriteLn('Loading Sounds...');
	
	Assign(inputFile, mapName + '\\Sounds.txt');
	Reset(inputFile);
	
	for index := Low(sBuffers) to High(sBuffers) do
	begin
		ReadLn(inputFile, temp); //Comment line, throw away
		ReadLn(inputFile, temp);
		sBuffers[index] := LoadWAVIntoBuffer(mapName + '\\' + temp);
	end;
	
	sSources[BACKGROUNDMUSIC] := CreateSoundSource(sBuffers[BACKGROUNDMUSIC], true, 0.7);
	sSources[WALKSOUND] := CreateSoundSource(sBuffers[WALKSOUND], true, 1.0);
	sSources[JETPACKSOUND] := CreateSoundSource(sBuffers[JETPACKSOUND], true, 1.0);
	sSources[COINSOUND] := CreateSoundSource(sBuffers[COINSOUND], false, 1.0);
	sSources[JETFUELCONTSOUND] := CreateSoundSource(sBuffers[JETFUELCONTSOUND], false, 1.0);
	sSources[GAMEWINSOUND] := CreateSoundSource(sBuffers[GAMEWINSOUND], false, 1.0);
	
	Close(inputFile);
end;


procedure LoadUI(mapName : String);
var
	inputFile : Text;
	index : Integer;
begin
	WriteLn('Loading UI...');
	
	Assign(inputFile, mapName + '\\UI.txt');
	Reset(inputFile);
	
	for index := Low(uiBitmaps) to High(uiBitmaps) do
	begin
		uiBitmaps[index] := LoadSingleBitmap(inputFile, mapName);
		uiSprites[index] := CreateSprite(uiBitmaps[index]);
	end;
	
	Close(inputFile);
end;


procedure LoadMapOptions(mapName : String);
var
	temp : String;
	inputFile : Text;
begin
	WriteLn('Loading Map Options...');
	
	Assign(inputFile, mapName + '\\MapOptions.txt');
	Reset(inputFile);
	
	ReadLn(inputFile, temp); //Discard comment line
	ReadLn(inputFile, nextMapName);
	ReadLn(inputFile, mapExitX);
	ReadLn(inputFile, mapExitY);
	ReadLn(inputFile, mapExitWidth);
	ReadLn(inputFile, mapExitHeight);
	
	Close(inputFile);
end;


procedure LoadBackgroundPanels(mapName : String);
var
	inputFile : Text;
	index, total, height, width, totalWidth : Integer;
	temp : String;
begin
	WriteLn('Loading Background Panels...');
	totalWidth := 0;
	
	Assign(inputFile, mapName + '\\BackgroundPanels.txt');
	Reset(inputFile);
	
	ReadLn(inputFile, total);
	SetLength(backgroundPanels, total);
	
	WriteLn('You specified ', total, ' background panels');
	
	ReadLn(inputFile, height);
	
	for index := Low(backgroundPanels) to High(backgroundPanels) do
	begin
		WriteLn('Loading panel ', index + 1);
	
		ReadLn(inputFile, temp); //discard comment 
		
		ReadLn(inputFile, temp);
		ReadLn(inputFile, width);
		
		backgroundPanels[index].bitmap := LoadBitmap(mapName + '\\' + temp, width, height, false, 255, GetColour(255, 0, 255));
		backgroundPanels[index].spr := CreateSprite(backgroundPanels[index].bitmap);
		backgroundPanels[index].spr.yPos := 0;
		backgroundPanels[index].spr.xPos := totalWidth;
		
		totalWidth := totalWidth + width;
	end;
	
	backgroundWidth := totalWidth;
	
	Close(inputFile);
end;


procedure UnloadMap();
var
	index : Integer;
begin
	for index := Low(backgroundPanels) to High(backgroundPanels) do
	begin
		FreeSprite(backgroundPanels[index].spr);
	end;

	for index := Low(objects) to High(objects) do
	begin
		FreeSprite(objects[index].spr);
	end;
	
	for index := Low(uiSprites) to High(uiSprites) do
	begin
		FreeSprite(uiSprites[index]);
	end;
	
	for index := Low(sBuffers) to High(sBuffers) do
	begin
		UnloadBuffer(sBuffers[index]);
	end;
	
	for index := Low(sSources) to High(sSources) do
	begin
		DeleteSoundSource(sSources[index]);
	end;
end;

	
procedure LoadMap(quick : Boolean);
begin	
	if not quick then WriteLn();
	if not quick then WriteLn('Loading ', mapName, '...');
	
	try
		if not quick then LoadMapOptions(mapName);
		if not quick then LoadBackgroundPanels(mapName);
		if not quick then LoadBitmaps(mapName);
		LoadWorldObjects(mapName);
		if not quick then LoadUI(mapName);
		if not quick then LoadSounds(mapName);
	except 
		on e : Exception do
		begin
			WriteLn('Map Loading Failed!');	
			CloseGraphicsWindow();
			UnloadMap();
			ReadLn();
			Halt(1);
		end;
	end;
	
	vwPrtX := 0;
	vwPrtY := 0;
	
	jetpackFuel := MAXJETPACKFUEL;
	playerScore := playerScoreOnStart;
	jetFuelContainers := jetFuelContainersOnStart;
	
	gameOver := NOTOVER;
end;
	

procedure Initialise();
begin
	quitRequested := false;		

	Write('Enter Map Name: ');
	ReadLn(mapName);
	
	playerScoreOnStart := 0;
	jetFuelContainersOnStart := 0;

	LoadMap(false);	

	OpenGraphicsWindow(GAMENAME, RESX,RESY);
end;


procedure Finalise();
begin
	CloseGraphicsWindow();
	
	UnloadMap();
end;


//
function TryMoveObject(var objectToMove : WorldObject; movementVector : Vector; actuallyMove : Boolean; out collidedWithIndex : Integer): Boolean;
var
	index : Integer;
begin
	collidedWithIndex := -1;

	MoveSprite(objectToMove.spr, movementVector);
	
	if HasSpriteCollidedX(objectToMove.spr, backgroundWidth + 1, CollisionRangeGreaterThan) or 
	   HasSpriteCollidedY(objectToMove.spr, RESY + 1, CollisionRangeGreaterThan) or 
	   HasSpriteCollidedY(objectToMove.spr, -1, CollisionRangeLessThan) or 
	   HasSpriteCollidedX(objectToMove.spr, -1, CollisionRangeLessThan) then
  	begin
  		result := false;
		MoveSprite(objectToMove.spr, InvertVector(movementVector));
		exit;
  	end;
	
	for index := 0 to High(objects) do
	begin
		if (@objectToMove <> @(objects[index])) and objectToMove.collidable and objects[index].collidable and not objects[index].dead then
		begin
			if HaveSpritesCollided(objectToMove.spr, objects[index].spr) then
			begin
				result := false;
				MoveSprite(objectToMove.spr, InvertVector(movementVector));
				collidedWithIndex := index;
				exit;
			end;
		end;
	end;
	
	if actuallyMove = false then
		MoveSprite(objectToMove.spr, InvertVector(movementVector));
	
	result := true;
	
end;


function TryMoveObjectCloseAsPossible(var objectToMove : WorldObject; movementVector : Vector; out collidedWithIndex : Integer): Vector;
var
	unitVector, tempVector, lastVector : Vector;
	retVal : Boolean;
	movVectMag : Single;
begin
	collidedWithIndex := -1;
	unitVector := GetUnitVector(movementVector);
	tempVector := CreateVector(0,0);
	lastVector := CreateVector(0,0);
	movVectMag := GetVectorMagnitude(movementVector);
	retVal := true;
	
	while (GetVectorMagnitude(tempVector) <= movVectMag) and 
		  retval = true and
		  (movVectMag <> 0) do
	begin
	
		lastVector := tempVector;
		
		tempVector := AddVectors(tempVector, unitVector);
		
		retVal := TryMoveObject(objectToMove, tempVector, false, collidedWithIndex);
	end;
	
	//If there was a collision
	if retVal = false then
	begin
		result := InvertVector(SubtractVectors(movementVector, lastVector));
		//Move with the calced vector
		MoveSprite(objectToMove.spr, lastVector);
	end
	else
	begin
		result := CreateVector(0,0);
		MoveSprite(objectToMove.spr, movementVector);
	end;
end;


procedure UseJetFuelContainer();
begin
	if (jetFuelContainers > 0) and (jetpackFuel < MAXJETPACKFUEL) then
	begin
		jetFuelContainers := jetFuelContainers - 1;
		jetpackFuel := MAXJETPACKFUEL;
	end;
end;


procedure ProcessKeyboardEvents();
begin
	if IsKeyPressed($10) and IsKeyPressed($52) then //Shift R
		LoadMap(true);
	
	if IsKeyPressed($25) then //Left arrow. Note $ sign means the literal is in hex
		objects[PLAYER].horzMvVector := AddVectors(objects[PLAYER].horzMvVector, CreateVector(LEFTWALK,0, true));
		
	if IsKeyPressed($27) then //Right arrow
		objects[PLAYER].horzMvVector := AddVectors(objects[PLAYER].horzMvVector, CreateVector(RIGHTWALK,0, true));
		
	{if IsKeyPressed($41) then //A key
		objects[PLAYER].horzMvVector := AddVectors(objects[PLAYER].horzMvVector, CreateVector(-2,0, true));
		
	if IsKeyPressed($44) then //D key
		objects[PLAYER].horzMvVector := AddVectors(objects[PLAYER].horzMvVector, CreateVector(2,0, true));}
		
	if IsKeyPressed($10) then //Shift key
	begin
		if objects[PLAYER].horzMvVector.x < 0 then
			objects[PLAYER].horzMvVector := AddVectors(objects[PLAYER].horzMvVector, CreateVector(LEFTRUN,0, true))
		else if objects[PLAYER].horzMvVector.x > 0 then
			objects[PLAYER].horzMvVector := AddVectors(objects[PLAYER].horzMvVector, CreateVector(RIGHTRUN,0, true));
			
	end;
	
	
	if IsKeyPressed($46) then //F key
	begin
		if fKeyPressed = false then UseJetFuelContainer(); //If the key isn't being held
	
		fKeyPressed := true; //Key is now being held
	end 
	else fKeyPressed := false; //Key is now not being held
			
	
	if IsKeyPressed($20) then //Space
		objects[PLAYER].JumpCounter := JUMPCOUNTER;
	
	if IsKeyPressed($51) then //Q
		quitRequested := true;
	
	if WindowCloseRequested() then
		quitRequested := true;
		
end;


procedure DrawJetpackFuelMeter();
var
	percentageFuel : Single;
	barColour : Colour;
begin
	percentageFuel := jetpackFuel / MAXJETPACKFUEL * 100;
	
	//Bar colour
	if percentageFuel < 40 then barColour := ColourRed
	else barColour := ColourGreen;
	
	DrawRectangle(barColour, true, 645, 5, trunc(percentageFuel * 1.5), 14);
	
	DrawText('Jetpack Fuel', ColourBlack, ArialFont, BoldFont, 15, 0, 565, 5);
	DrawRectangle(ColourBlack, false, 645, 5, 150, 14);
	DrawRectangle(ColourBlack, false, 646, 6, 148, 12);
end;


procedure DrawGameOver();
var
	x, y : Integer;
begin
	if gameOver = PLAYERLOSE then
	begin
		x := 190;
		y := 240;
		
		DrawRectangle(ColourBlack, true, x, y, 410, 100);
		DrawRectangle(ColourRed, false, x, y, 410, 100);
		DrawRectangle(ColourRed, false, x + 1, y + 1, 408, 98);
		DrawRectangle(ColourRed, false, x + 2, y + 2, 406, 96);
		DrawText('Game Over', ColourRed, ArialFont, BoldFont, 80, 0, x + 20, y + 10);
	end
	else if gameOver = PLAYERWIN then
	begin
		x := 240;
		y := 240;
		
		DrawRectangle(ColourBlack, true, x, y, 320, 100);
		DrawRectangle(ColourGreen, false, x, y, 320, 100);
		DrawRectangle(ColourGreen, false, x + 1, y + 1, 318, 98);
		DrawRectangle(ColourGreen, false, x + 2, y + 2, 316, 96);
		DrawText('You Win', ColourGreen, ArialFont, BoldFont, 80, 0, x + 20, y + 10);
	end;
end;


procedure DrawPlayerJetpackFuelContainers();
var
	temp : String;
	index : Integer;
begin
	Str(jetFuelContainers, temp);
	DrawText('Jet Fuel Boosts:', ColourBlack, ArialFont, BoldFont, 15, 0, 547, 27);
	//DrawText(temp, ColourBlack, ArialFont, BoldFont, 15, 0, 643, 27);
	for index := 1 to jetFuelContainers do
	begin
		MoveSpriteTo(uiSprites[JETFUELCONTICON], 644 + (19 * (index - 1)), 27);
		DrawSprite(uiSprites[JETFUELCONTICON]);
	end;
end;


procedure DrawPlayerScore();
var
	temp : String;
begin
	Str(playerScore, temp);
	DrawText('Score:', ColourBlack, ArialFont, BoldFont, 15, 0, 600, 47);
	DrawText(temp, ColourBlack, ArialFont, BoldFont, 15, 0, 643, 47);
end;


procedure DrawUIBackground();
begin
	MoveSpriteTo(uiSprites[UIBACKGROUNDIMG], 541, 0);
	DrawSprite(uiSprites[UIBACKGROUNDIMG]);
end;


procedure DoSceneRedraw(const dataFPSInfo, renderFPSInfo : FPSCalcInfo);
var
	index : Integer;
	temp, temp2, temp3 : String;
	textColour : Colour;
	average, highest, lowest : Single;
	x,y : Integer;
begin	
	
	for index := Low(backgroundPanels) to High(backgroundPanels) do
	begin
		DrawSprite(backgroundPanels[index].spr, vwPrtX, vwPrtY, RESX, RESY);
		
		if IsKeyPressed($10) and IsKeyPressed($49) and //Shift I
			   not IsSpriteOffscreen(backgroundPanels[index].spr, vwPrtX, vwPrtY, RESX, RESY) then 
			begin
				Str(index, temp);
				x := trunc(backgroundPanels[index].spr.xPos) - vwPrtX;
				y := 100;
				
				DrawText(temp, ColourBlack, ArialFont, BoldFont, 30, 0, x, y);
			end;
	end;

	for index := 0 to High(objects) do
	begin
		if not objects[index].dead then
		begin
			DrawSprite(objects[index].spr, vwPrtX, vwPrtY, RESX, RESY);
			
			//Map Construction Aid. Shows the index for each object onscreen.
			if IsKeyPressed($10) and IsKeyPressed($49) and //Shift I
			   not IsSpriteOffscreen(objects[index].spr, vwPrtX, vwPrtY, RESX, RESY) then 
			begin
				Str(index, temp);
				x := trunc(objects[index].spr.xPos) - vwPrtX;
				y := trunc(objects[index].spr.yPos) - vwPrtY;
				
				if y + 23 > RESY then y := RESY - 17;
				
				DrawText(temp, ColourBlack, ArialFont, BoldFont, 17, 0, x, y);
			end;
		end;
	end;
	
	//Draw framerates
	DrawRectangle(ColourBlack, true, 0,0, 190, 33);
	
	average := (1000000 / dataFPSInfo.average);
	lowest     := (1000000 / dataFPSInfo.high);
	highest    := (1000000 / dataFPSInfo.low);
	if (average < 57) or (average > 67) then textColour := ColourRed //Within 5
	else textColour := ColourGreen; //Should run at 62.5fps
	Str(average:4:1, temp);
	Str(highest:4:1, temp2);
	Str(lowest:4:1, temp3);
	DrawText('Data FPS: (' + temp3 + ', ' + temp2 + ') ' + temp, textColour, ArialFont, 0, 15, 0, 2, 0);
	
	average := (1000000 / renderFPSInfo.average);
	lowest     := (1000000 / renderFPSInfo.high);
	highest    := (1000000 / renderFPSInfo.low);
	if average < 30 then textColour := ColourRed
	else if average < 50 then textColour := ColourYellow
	else textColour := ColourGreen;
	Str(average:4:1, temp);
	Str(highest:4:1, temp2);
	Str(lowest:4:1, temp3);
	DrawText('Render FPS: (' + temp3 + ', ' + temp2 + ') ' + temp, textColour, ArialFont, 0, 15, 0, 2, 17);
	
	DrawUIBackground();
	
	DrawJetpackFuelMeter();
	
	DrawPlayerJetpackFuelContainers();
	
	DrawPlayerScore();
	
	DrawGameOver();
	
	RefreshScreen();
end;


procedure DoLeftRightAI(var theObject : WorldObject);
begin
	//If the AI isn't moving horizontally, get it to start moving left
	if theObject.horzMvVector.x = 0 then theObject.horzMvVector := CreateVector(LEFTWALK, 0);
end;


procedure DoCoinAI(var theObject : WorldObject);
begin
	if HaveSpritesCollided(theObject.spr, objects[PLAYER].spr) = true then
	begin
		theObject.dead := true;
		playerScore := playerScore + COINWORTH;
		
		if IsSoundSourcePlaying(sSources[COINSOUND]) then
			StopSoundSource(sSources[COINSOUND]);
		PlaySoundSource(sSources[COINSOUND]);
	end;
end;


procedure DoJetFuelContainerAI(var theObject : WorldObject);
begin
	if (HaveSpritesCollided(theObject.spr, objects[PLAYER].spr) = true) and
	   (jetFuelContainers < MAXJETFUELCONTS) then
	begin
		theObject.dead := true;
		jetFuelContainers := jetFuelContainers + 1;
		
		if IsSoundSourcePlaying(sSources[JETFUELCONTSOUND]) then
			StopSoundSource(sSources[JETFUELCONTSOUND]);
		PlaySoundSource(sSources[JETFUELCONTSOUND]);
	end;
end;


procedure DoAIs();
var
	index : Integer;
begin
	for index := 0 to High(objects) do
	begin
		if not objects[index].dead then
		begin
		
			if objects[index].objectType = LEFTRIGHTAI then
				DoLeftRightAI(objects[index])
			else if objects[index].objectType = COINAI then
				DoCoinAI(objects[index])
			else if objects[index].objectType = JETFUELCONTAI then
				DoJetFuelContainerAI(objects[index]);
		
		end;
	end;
end;


procedure DoLeftRightAICollision(var theObject : WorldObject; movementVector, collisionVector : Vector; collidedWithIndex : Integer);
begin
	//If there is a collision
	if not IsZeroVector(collisionvector) and (GetVectorMagnitude(movementVector) >= 1) then
	begin
		//Reverse the movement
		theObject.horzMvVector.x := theObject.horzMvVector.x * -1;
		
		if collidedWithIndex = PLAYER then gameOver := PLAYERLOSE;
	end;
end;


procedure DoAICollisionHooks(var theObject : WorldObject; movementVector, collisionVector : Vector; collidedWithIndex : Integer);
begin
	if theObject.objectType = LEFTRIGHTAI then
	begin
		DoLeftRightAICollision(theObject, movementVector, collisionVector, collidedWithIndex);
	end;
end;


procedure ApplyHorizontalMovement();
var
	index : Integer;
	resultVector : Vector;
	collidedWithIndex : Integer;
begin
	for index := 0 to High(objects) do
	begin
	
		if not objects[index].dead then
		begin
			resultVector := TryMoveObjectCloseAsPossible(objects[index], objects[index].horzMvVector, collidedWithIndex);	
			
			//Detect player getting killed
			if collidedWithIndex <> -1 then
				if (index = PLAYER) and objects[collidedWithIndex].killsPlayer then gameOver := PLAYERLOSE;
			
			//AI collision hooks
			DoAICollisionHooks(objects[index], objects[index].horzMvVector, resultVector, collidedWithIndex);
		end;
	
	end;
end;


procedure DoMovementDecay();
var
	index : Integer;
begin
	for index := 0 to High(objects) do
	begin
		
		if not objects[index].dead then
		begin
			objects[index].vertMvVector := DecayVector(objects[index].vertMvVector, objects[index].decayProps);
			objects[index].horzMvVector := DecayVector(objects[index].horzMvVector, objects[index].decayProps);
		end;
	
	end;
end;


procedure DoJumping();
var
	index : Integer;
begin
	for index := 0 to High(objects) do
	begin
		if not objects[index].dead and
		   (objects[index].jumpCounter > 0) then
		begin
			//Don't do jumping if the player has run out of fuel
			if (index = PLAYER) and (jetpackFuel <= 0) then continue
			else jetpackFuel := jetpackFuel - JETPACKUSEPERCYCLE;
			
			objects[index].vertMvVector := AddVectors(objects[index].vertMvVector, CreateVector(0,JUMP, true));
			objects[index].jumpCounter := objects[index].jumpCounter - 1;
		end;
	end;
end;


procedure ApplyGravity();
var
	index, collidedWithIndex : Integer;
	resultVector : Vector;
begin
	for index := 0 to High(objects) do
	begin
	
		if not objects[index].dead and 
		   objects[index].affectedByGravity then
		begin
			objects[index].vertMvVector := AddVectors(objects[index].vertMvVector, CreateVector(0, GRAVITY, true));
			
			resultVector := TryMoveObjectCloseAsPossible(objects[index], objects[index].vertMvVector, collidedWithIndex);
			
			//If gravity can't move the object because of a collision (not a zero difference vector and not a subpixel difference vector)
			if (IsZeroVector(resultVector) = false) and
				(GetVectorMagnitude(objects[index].vertMvVector) >= 1)
				then
			begin
				//Cancel the gravity
				objects[index].vertMvVector.y := 0;
			end;
			
			//Detect player getting killed
			if collidedWithIndex <> -1 then
				if (index = PLAYER) and objects[collidedWithIndex].killsPlayer then gameOver := PLAYERLOSE;
			
			//Do the AI collision hooks
			DoAICollisionHooks(objects[index], objects[index].vertMvVector, resultVector, collidedWithIndex);
		end;
			
	end;
end;


procedure DoViewportMove();
begin
	if HasSpriteCollidedX(objects[PLAYER].spr, VWPRTMOVEATX, CollisionRangeGreaterThan) then
	begin
		vwPrtX := Trunc(objects[PLAYER].spr.xPos) + objects[PLAYER].spr.bitmaps[objects[PLAYER].spr.currentFrame].width - (RESX div 2);
		
		if vwPrtX < 0 then 
			vwPrtX := 0;
			
		if (vwPrtX + RESX) > backgroundWidth then
			vwPrtX := backgroundWidth - RESX;
	end;
end;


function IsAnimationLeftFacing(const theObject : WorldObject): Boolean;
begin
	result := (theObject.aniProps.currentAniMode = WALKLEFT) or (theObject.aniProps.currentAniMode = STANDLEFT) or (theObject.aniProps.currentAniMode = JUMPLEFT) or (theObject.aniProps.currentAniMode = JUMPHMOVLEFT);
end;


procedure IncrementAnimationIndex(var theObject : WorldObject; newAniMode : Integer);
begin
	if theObject.aniProps.currentAniMode = newAniMode then
	begin
		theObject.spr.currentFrame := theObject.spr.currentFrame + 1;
		
		//Don't let it increment past the end index
		if theObject.spr.currentFrame > theObject.aniProps.indexes[newAniMode].endIndex then
		begin
			theObject.spr.currentFrame := theObject.aniProps.indexes[newAniMode].startIndex;
		end;
	end
	else
	begin
		theObject.spr.currentFrame := theObject.aniProps.indexes[newAniMode].startIndex;
		
		theObject.aniProps.currentAniMode := newAniMode;
	end;
end;


procedure DoWorldObjectAnimation(var theObject : WorldObject);
begin
	if (theObject.horzMvVector.x < 0) and (theObject.vertMvVector.y = 0) then //Moving Left, not moving vertically
	begin
		IncrementAnimationIndex(theObject, WALKLEFT)
	end
	else if (theObject.horzMvVector.x < 0) and (theObject.vertMvVector.y <> 0) then //Moving Left, moving vertically
	begin
		IncrementAnimationIndex(theObject, JUMPHMOVLEFT)
	end
	else if (theObject.horzMvVector.x > 0) and (theObject.vertMvVector.y = 0) then //Moving Right, not moving vertically
	begin
		IncrementAnimationIndex(theObject, WALKRIGHT)
	end
	else if (theObject.horzMvVector.x > 0) and (theObject.vertMvVector.y <> 0) then //Moving Right, moving vertically
	begin
		IncrementAnimationIndex(theObject, JUMPHMOVRIGHT)
	end
	else if (theObject.horzMvVector.x = 0) and (theObject.vertMvVector.y = 0) then //Not moving horizontally, not moving vertically
	begin
		if IsAnimationLeftFacing(theObject) then
			IncrementAnimationIndex(theObject, STANDLEFT)
		else
			IncrementAnimationIndex(theObject, STANDRIGHT);
	end
	else if (theObject.horzMvVector.x = 0) and (theObject.vertMvVector.y <> 0) then //Not moving horizontally, moving vertically
	begin
		if IsAnimationLeftFacing(theObject) then
			IncrementAnimationIndex(theObject, JUMPLEFT)
		else
			IncrementAnimationIndex(theObject, JUMPRIGHT);
	end;
end;


procedure DoAnimations();
var
	index : Integer;
begin
	for index := 0 to High(objects) do
	begin
		
		if not objects[index].dead then
		begin
			
			DoWorldObjectAnimation(objects[index]);
				
		end;
		
	end;
end;


procedure RechargeJetpack();
begin
	if not IsKeyPressed($20) then //Spacebar key
	begin
		jetpackFuel := jetpackFuel + JETPACKRECHARGE;
		
		if jetpackFuel > MAXJETPACKFUEL then
			jetpackFuel := MAXJETPACKFUEL;
	end;
end;


procedure DoSounds();
begin
	if gameOver <> NOTOVER then //If the game is over
	begin
		StopSoundSource(sSources[WALKSOUND]);
		StopSoundSource(sSources[JETPACKSOUND]);
		StopSoundSource(sSources[COINSOUND]);
		StopSoundSource(sSources[JETFUELCONTSOUND]);
		exit;
	end;
	
	if (objects[PLAYER].horzMvVector.x <> 0) and (objects[PLAYER].vertMvVector.y = 0) then
	begin
		if not IsSoundSourcePlaying(sSources[WALKSOUND]) then PlaySoundSource(sSources[WALKSOUND]);
		if not IsSoundSourcePaused(sSources[JETPACKSOUND]) then PauseSoundSource(sSources[JETPACKSOUND]);
	end
	else if (objects[PLAYER].vertMvVector.y <> 0) and (jetpackFuel > 0) then
	begin
		if not IsSoundSourcePlaying(sSources[JETPACKSOUND]) then PlaySoundSource(sSources[JETPACKSOUND]);
		if not IsSoundSourcePaused(sSources[WALKSOUND]) then PauseSoundSource(sSources[WALKSOUND]);
	end
	else
	begin
		if not IsSoundSourcePaused(sSources[JETPACKSOUND]) then PauseSoundSource(sSources[JETPACKSOUND]);
		if not IsSoundSourcePaused(sSources[WALKSOUND]) then PauseSoundSource(sSources[WALKSOUND]);
	end;
end;


procedure DoMapExitDetection();
begin
	if HasSpriteCollidedWithRect(objects[PLAYER].spr, mapExitX, mapExitY, mapExitWidth, mapExitHeight) = true then
	begin
		//If there is no next map
		if nextMapName = 'None' then 
		begin
			gameOver := PLAYERWIN;
			PlaySoundSource(sSources[GAMEWINSOUND]);
			exit;
		end;
		
		playerScoreOnStart := playerScore;
		jetFuelContainersOnStart := jetFuelContainers;
		
		UnloadMap();
		mapName := nextMapName;
		LoadMap(false);
		
		if not IsSoundSourcePlaying(sSources[BACKGROUNDMUSIC]) then PlaySoundSource(sSources[BACKGROUNDMUSIC]);
		
	end;
end;


function GetRunningAverage(var runningArray : Array of Single; newNumber : Single; var index : Integer): Single;
var
	loopCount : Integer;
	sum : Double;
begin
	sum := 0;
	
	runningArray[index] := newNumber;
	for loopCount := Low(runningArray) to High(runningArray) do
	begin
		sum := sum + runningArray[loopCount];
	end;
	
	result := sum / Length(runningArray);

	//Inc index
	index := index + 1;
	if index > High(runningArray) then index := Low(runningArray);
end;


procedure ClearArray(var theArray: Array of Single);
var
	index : Integer;
begin
	for index := Low(theArray) to High(theArray) do
	begin
		theArray[index] := 0.0;
	end;
end;


procedure InitFPSCalcInfo(var fpsInfo : FPSCalcInfo);
begin
	ClearArray(fpsInfo.valuesArray); 
	fpsInfo.arrayIndex := 0; 
	fpsInfo.loopCount := 0; 
	fpsInfo.High := 1; 
	fpsInfo.Low := 1; 
	fpsInfo.Average := 1000000;
end;


procedure DoFPSCalculations(var fpsInfo : FPSCalcInfo; const nowTime, lastUpdateTime : Int64);
begin
	//Populate the running average for the first 10 calcs
	if fpsInfo.loopCount < 10 then
	begin
		//Don't get the avg, because this may result in a div 0 error if avg is 0 (when calcing 1000000/avg)
		GetRunningAverage(fpsInfo.valuesArray, nowTime - lastUpdateTime, fpsInfo.arrayIndex);
		fpsInfo.loopCount := fpsInfo.loopCount + 1;
	end
	//All other calcs get the average as normal
	else fpsInfo.average := GetRunningAverage(fpsInfo.valuesArray, nowTime - lastUpdateTime, fpsInfo.arrayIndex);
	
	//On the 10th calcs set the min maxes
	if fpsInfo.loopCount = 10 then
	begin
		//Get the running average
		fpsInfo.average := GetRunningAverage(fpsInfo.valuesArray, nowTime - lastUpdateTime, fpsInfo.arrayIndex);;
		fpsInfo.High := fpsInfo.average;
		fpsInfo.Low := fpsInfo.average;
		fpsInfo.loopCount := fpsInfo.loopCount + 1;
	end;
	
	//Adjust the min/maxes
	if fpsInfo.average > fpsInfo.High then fpsInfo.High := fpsInfo.average;
	if fpsInfo.average < fpsInfo.Low then fpsInfo.Low := fpsInfo.average;
end;


procedure DoGameLoop;
var
	lastDataUpdateTime, lastDrawUpdateTime, nowTime, renderTimeLength : Int64;
	
	dataFPSInfo, renderFPSInfo : FPSCalcInfo;
	
	aniDelayCount : Integer;
	
	//temp : String;
	//output : Text;
begin
	InitFPSCalcInfo(dataFPSInfo);
	InitFPSCalcInfo(renderFPSInfo);
	lastDataUpdateTime := GetPerformanceCounter();
	lastDrawUpdateTime := lastDataUpdateTime;
	renderTimeLength := 0;
	aniDelayCount := 0;
	
	//Start background music
	PlaySoundSource(sSources[BACKGROUNDMUSIC]);

	while quitRequested = false do
	begin
		nowTime := GetPerformanceCounter();
	
		if (nowTime - lastDataUpdateTime > DATAUPDATERATE) then
		begin			
			DoFPSCalculations(dataFPSInfo, nowTime, lastDataUpdateTime);
		
			lastDataUpdateTime := nowTime;
		
			if (gameOver = NOTOVER) then 
			begin
				RechargeJetpack();
				DoMovementDecay();
			end;
					
			ProcessKeyboardEvents();
			
			if (gameOver = NOTOVER) then
			begin
				DoJumping();
				DoAIs();
				ApplyHorizontalMovement();
				ApplyGravity();
				
				aniDelayCount := aniDelayCount + 1;
				if aniDelayCount > 7 then
				begin
					DoAnimations();
					aniDelayCount := 0;
				end;
				
				DoViewportMove();
				
				DoMapExitDetection();
			end;
			
			DoSounds(); 
		end;
		
		nowTime := GetPerformanceCounter();
		if (nowTime - lastDataUpdateTime + renderTimeLength < DATAUPDATERATE) and (nowTime - lastDrawUpdateTime > RENDERMAXRATE) or //Capped at 125, minimum 31.25 fps
		   (nowTime - lastDrawUpdateTime > RENDERMINRATE) then
		begin
		
			DoFPSCalculations(renderFPSInfo, nowTime, lastDrawUpdateTime);
		
			lastDrawUpdateTime := nowTime;
			
			DoSceneRedraw(dataFPSInfo, renderFPSInfo);
			
			//Calc time taken to render
			renderTimeLength := GetPerformanceCounter() - lastDrawUpdateTime;
			
			//Str(GetPerformanceCounter() - lastDrawUpdateTime, temp);
			//debugDump := debugDump + 'Render time taken: ' + temp + #13 + #10;
		end;
		
	end;
	
	{Assign(output, 'dump.txt');
	Rewrite(output);
	Write(output, debugDump);
	Close(output);}
end;


procedure Main();
begin
	Initialise();
	
	DoGameLoop();
	
	Finalise();
end;


begin
	Main();
	readln();
end.